/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.drools.workbench.screens.guided.dtree.client.widget;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.inject.Inject;

import com.ait.lienzo.client.core.animation.AnimationProperties;
import com.ait.lienzo.client.core.animation.AnimationTweener;
import com.ait.lienzo.client.core.animation.IAnimation;
import com.ait.lienzo.client.core.animation.IAnimationCallback;
import com.ait.lienzo.client.core.animation.IAnimationHandle;
import com.ait.lienzo.client.core.shape.Group;
import com.ait.lienzo.client.core.shape.Rectangle;
import com.ait.lienzo.client.core.shape.Text;
import com.ait.lienzo.client.core.types.Point2D;
import com.ait.lienzo.shared.core.types.TextAlign;
import com.ait.lienzo.shared.core.types.TextBaseLine;
import com.google.gwt.user.client.Window;
import org.drools.workbench.models.guided.dtree.shared.model.GuidedDecisionTree;
import org.drools.workbench.models.guided.dtree.shared.model.nodes.ActionInsertNode;
import org.drools.workbench.models.guided.dtree.shared.model.nodes.ActionRetractNode;
import org.drools.workbench.models.guided.dtree.shared.model.nodes.ActionUpdateNode;
import org.drools.workbench.models.guided.dtree.shared.model.nodes.ConstraintNode;
import org.drools.workbench.models.guided.dtree.shared.model.nodes.Node;
import org.drools.workbench.models.guided.dtree.shared.model.nodes.TypeNode;
import org.drools.workbench.screens.guided.dtree.client.editor.GuidedDecisionTreeEditorPresenter;
import org.drools.workbench.screens.guided.dtree.client.resources.i18n.GuidedDecisionTreeConstants;
import org.drools.workbench.screens.guided.dtree.client.widget.factories.ActionInsertNodeFactory;
import org.drools.workbench.screens.guided.dtree.client.widget.factories.ActionRetractNodeFactory;
import org.drools.workbench.screens.guided.dtree.client.widget.factories.ActionUpdateNodeFactory;
import org.drools.workbench.screens.guided.dtree.client.widget.factories.ConstraintNodeFactory;
import org.drools.workbench.screens.guided.dtree.client.widget.factories.TypeNodeFactory;
import org.drools.workbench.screens.guided.dtree.client.widget.shapes.BaseGuidedDecisionTreeShape;
import org.drools.workbench.screens.guided.dtree.client.widget.shapes.TypeShape;
import org.uberfire.client.mvp.UberView;
import org.uberfire.commons.data.Pair;
import org.uberfire.ext.wires.core.api.events.ClearEvent;
import org.uberfire.ext.wires.core.api.events.ShapeAddedEvent;
import org.uberfire.ext.wires.core.api.events.ShapeDeletedEvent;
import org.uberfire.ext.wires.core.api.events.ShapeDragCompleteEvent;
import org.uberfire.ext.wires.core.api.events.ShapeDragPreviewEvent;
import org.uberfire.ext.wires.core.api.events.ShapeSelectedEvent;
import org.uberfire.ext.wires.core.api.layout.LayoutManager;
import org.uberfire.ext.wires.core.api.layout.RequiresLayoutManager;
import org.uberfire.ext.wires.core.api.shapes.WiresBaseShape;
import org.uberfire.ext.wires.core.client.canvas.WiresCanvas;
import org.uberfire.ext.wires.core.client.util.ShapeFactoryUtil;
import org.uberfire.ext.wires.core.trees.client.canvas.WiresTreeNodeConnector;
import org.uberfire.ext.wires.core.trees.client.layout.WiresLayoutUtilities;
import org.uberfire.ext.wires.core.trees.client.layout.treelayout.Rectangle2D;
import org.uberfire.ext.wires.core.trees.client.shapes.WiresBaseTreeNode;

public class GuidedDecisionTreeWidget extends WiresCanvas implements UberView<GuidedDecisionTreeEditorPresenter> {

    private static final int MAX_PROXIMITY = 200;

    private static final int ANIMATION_DURATION = 250;

    private Event<ClearEvent> clearEvent;
    private Event<ShapeSelectedEvent> shapeSelectedEvent;
    private Event<ShapeAddedEvent> shapeAddedEvent;
    private Event<ShapeDeletedEvent> shapeDeletedEvent;
    private LayoutManager layoutManager;
    private TypeNodeFactory typeNodeFactory;
    private ConstraintNodeFactory constraintNodeFactory;
    private ActionInsertNodeFactory actionInsertNodeFactory;
    private ActionUpdateNodeFactory actionUpdateNodeFactory;
    private ActionRetractNodeFactory actionRetractNodeFactory;

    private GuidedDecisionTreeDropContext dropContext = new GuidedDecisionTreeDropContext();

    private WiresTreeNodeConnector connector = null;

    private WiresBaseTreeNode uiRoot;
    private GuidedDecisionTree model;

    private GuidedDecisionTreeEditorPresenter presenter;

    private Group hint = null;
    private boolean isGettingStartedHintVisible = false;

    public GuidedDecisionTreeWidget() {
        //CDI proxy
    }

    @Inject
    public GuidedDecisionTreeWidget(final Event<ClearEvent> clearEvent,
                                    final Event<ShapeSelectedEvent> shapeSelectedEvent,
                                    final Event<ShapeAddedEvent> shapeAddedEvent,
                                    final Event<ShapeDeletedEvent> shapeDeletedEvent,
                                    final LayoutManager layoutManager,
                                    final TypeNodeFactory typeNodeFactory,
                                    final ConstraintNodeFactory constraintNodeFactory,
                                    final ActionInsertNodeFactory actionInsertNodeFactory,
                                    final ActionUpdateNodeFactory actionUpdateNodeFactory,
                                    final ActionRetractNodeFactory actionRetractNodeFactory) {
        this.clearEvent = clearEvent;
        this.shapeSelectedEvent = shapeSelectedEvent;
        this.shapeAddedEvent = shapeAddedEvent;
        this.shapeDeletedEvent = shapeDeletedEvent;
        this.layoutManager = layoutManager;
        this.typeNodeFactory = typeNodeFactory;
        this.constraintNodeFactory = constraintNodeFactory;
        this.actionInsertNodeFactory = actionInsertNodeFactory;
        this.actionUpdateNodeFactory = actionUpdateNodeFactory;
        this.actionRetractNodeFactory = actionRetractNodeFactory;
    }

    @Override
    public void init(final GuidedDecisionTreeEditorPresenter presenter) {
        this.presenter = presenter;
    }

    @Override
    public void selectShape(final WiresBaseShape shape) {
        shapeSelectedEvent.fire(new ShapeSelectedEvent(shape));
    }

    public void onShapeSelected(@Observes ShapeSelectedEvent event) {
        final WiresBaseShape shape = event.getShape();
        super.selectShape(shape);
    }

    @Override
    public void deselectShape(final WiresBaseShape shape) {
        super.deselectShape(shape);
    }

    public void onDragPreviewHandler(@Observes ShapeDragPreviewEvent shapeDragPreviewEvent) {
        //We can only connect WiresTreeNodes to each other
        if (!(shapeDragPreviewEvent.getShape() instanceof BaseGuidedDecisionTreeShape)) {
            dropContext.setContext(null);
            return;
        }

        //Find a Parent Node to attach the Shape to
        final double cx = getX(shapeDragPreviewEvent.getX());
        final double cy = getY(shapeDragPreviewEvent.getY());
        final BaseGuidedDecisionTreeShape uiChild = (BaseGuidedDecisionTreeShape) shapeDragPreviewEvent.getShape();
        final BaseGuidedDecisionTreeShape uiProspectiveParent = getParentNode(uiChild,
                                                                              cx,
                                                                              cy);

        //If there is a prospective parent show the line between child and parent
        if (uiProspectiveParent != null) {
            if (connector == null) {
                connector = new WiresTreeNodeConnector();
                canvasLayer.add(connector);
                connector.moveToBottom();
            }
            connector.getPoints().get(0).set(uiProspectiveParent.getLocation());
            connector.getPoints().get(1).set(new Point2D(cx,
                                                         cy));
        } else if (connector != null) {
            canvasLayer.remove(connector);
            connector = null;
        }

        dropContext.setContext(uiProspectiveParent);
        canvasLayer.batch();
    }

    public void onDragCompleteHandler(@Observes ShapeDragCompleteEvent shapeDragCompleteEvent) {
        final WiresBaseShape wiresShape = shapeDragCompleteEvent.getShape();

        //Hide the temporary connector
        if (connector != null) {
            canvasLayer.remove(connector);
            canvasLayer.batch();
            connector = null;
        }

        //If there's no Shape to add then exit
        if (wiresShape == null) {
            dropContext.setContext(null);
            return;
        }

        //If the Shape is not intended for the Guided Decision Tree widget then exit
        if (!(wiresShape instanceof BaseGuidedDecisionTreeShape)) {
            dropContext.setContext(null);
            return;
        }
        final BaseGuidedDecisionTreeShape uiChild = (BaseGuidedDecisionTreeShape) wiresShape;

        //Get Shape's co-ordinates relative to the Canvas
        final double cx = getX(shapeDragCompleteEvent.getX());
        final double cy = getY(shapeDragCompleteEvent.getY());

        //If the Shape was dropped outside the bounds of the Canvas then exit
        if (cx < 0 || cy < 0) {
            dropContext.setContext(null);
            return;
        }

        final int scrollWidth = getElement().getScrollWidth();
        final int scrollHeight = getElement().getScrollHeight();
        if (cx > scrollWidth || cy > scrollHeight) {
            dropContext.setContext(null);
            return;
        }

        //Add the new Node to it's parent (unless this is the first node)
        final BaseGuidedDecisionTreeShape uiParent = dropContext.getContext();
        boolean addShape = ((getShapesInCanvas().size() == 0 && (uiChild instanceof TypeShape)) || (getShapesInCanvas().size() > 0 && uiParent != null));
        boolean addChildToParent = uiParent != null;

        if (addShape) {
            uiChild.setX(cx);
            uiChild.setY(cy);

            if (addChildToParent) {
                uiParent.addChildNode(uiChild);
                uiParent.getModelNode().addChild(uiChild.getModelNode());
            } else if (uiChild instanceof TypeShape) {
                uiRoot = uiChild;
                model.setRoot(((TypeShape) uiChild).getModelNode());
            }

            addShape(uiChild);

            //Notify other Panels of a Shape being added
            shapeAddedEvent.fire(new ShapeAddedEvent(uiChild));
        }
    }

    private double getX(double xShapeEvent) {
        return xShapeEvent - getAbsoluteLeft();
    }

    private double getY(double yShapeEvent) {
        return yShapeEvent - getAbsoluteTop();
    }

    @Override
    public void clear() {
        if (Window.confirm(GuidedDecisionTreeConstants.INSTANCE.confirmDeleteDecisionTree())) {
            super.clear();
            clearEvent.fire(new ClearEvent());
            uiRoot = null;
        }
    }

    @Override
    public void deleteShape(final WiresBaseShape shape) {
        if (confirmShapeDeletion()) {

            if (uiRoot != null && uiRoot.equals(shape)) {
                uiRoot = null;
                model.setRoot(null);

                shapeDeletedEvent.fire(new ShapeDeletedEvent(shape));
            } else if (shape instanceof BaseGuidedDecisionTreeShape) {
                final BaseGuidedDecisionTreeShape uiChild = (BaseGuidedDecisionTreeShape) shape;
                if (uiChild.getParentNode() instanceof BaseGuidedDecisionTreeShape) {
                    final BaseGuidedDecisionTreeShape uiParent = (BaseGuidedDecisionTreeShape) uiChild.getParentNode();
                    uiParent.getModelNode().removeChild(uiChild.getModelNode());
                }

                shapeDeletedEvent.fire(new ShapeDeletedEvent(shape));

                layout();
            }
        }
    }

    boolean confirmShapeDeletion() {
        return Window.confirm(GuidedDecisionTreeConstants.INSTANCE.confirmDeleteDecisionTreeNode());
    }

    @Override
    public void forceDeleteShape(final WiresBaseShape shape) {
        shapeDeletedEvent.fire(new ShapeDeletedEvent(shape));
    }

    public void onShapeDeleted(@Observes ShapeDeletedEvent event) {
        super.deleteShape(event.getShape());
        if (getShapesInCanvas().isEmpty()) {
            showGettingStartedHint();
        }
    }

    @Override
    public void addShape(final WiresBaseShape shape) {
        super.addShape(shape);

        //Attach relevant handlers
        if (shape instanceof RequiresLayoutManager) {
            ((RequiresLayoutManager) shape).setLayoutManager(layoutManager);
        }
        if (shape instanceof BaseGuidedDecisionTreeShape) {
            ((BaseGuidedDecisionTreeShape) shape).setPresenter(presenter);
        }

        if (!getShapesInCanvas().isEmpty()) {
            hideGettingStartedHint();
        }

        layout();
    }

    public void setModel(final GuidedDecisionTree model,
                         final boolean isReadOnly) {
        this.uiRoot = null;
        this.model = model;

        //Clear existing state
        super.clear();
        clearEvent.fire(new ClearEvent());

        //Walk model creating UIModel
        final TypeNode root = model.getRoot();
        if (root != null) {
            final WiresBaseTreeNode uiRoot = typeNodeFactory.getShape(root,
                                                                      isReadOnly);
            this.uiRoot = uiRoot;

            processChildren(root,
                            uiRoot,
                            isReadOnly);

            final Map<WiresBaseShape, Point2D> layout = layoutManager.getLayoutInformation(uiRoot);
            final Rectangle2D canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas(layout);
            for (Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet()) {
                final Point2D destination = new Point2D(e.getValue().getX(),
                                                        e.getValue().getY());

                e.getKey().setLocation(destination);
            }

            WiresLayoutUtilities.resizeViewPort(canvasBounds,
                                                canvasLayer.getViewport());
        }

        if (shapesInCanvas.isEmpty()) {
            showGettingStartedHint();
        }

        canvasLayer.batch();
    }

    private void processChildren(final Node node,
                                 final WiresBaseTreeNode uiNode,
                                 final boolean isReadOnly) {
        uiNode.setSelectionManager(this);
        uiNode.setShapesManager(this);
        uiNode.setLayoutManager(layoutManager);
        if (uiNode instanceof BaseGuidedDecisionTreeShape) {
            ((BaseGuidedDecisionTreeShape) uiNode).setPresenter(presenter);
        }
        canvasLayer.add(uiNode);
        shapesInCanvas.add(uiNode);

        final Iterator<Node> itr = node.iterator();
        while (itr.hasNext()) {
            final Node child = itr.next();
            WiresBaseTreeNode uiChildNode = null;
            if (child instanceof TypeNode) {
                uiChildNode = typeNodeFactory.getShape((TypeNode) child,
                                                       isReadOnly);
            } else if (child instanceof ConstraintNode) {
                uiChildNode = constraintNodeFactory.getShape((ConstraintNode) child,
                                                             isReadOnly);
            } else if (child instanceof ActionInsertNode) {
                uiChildNode = actionInsertNodeFactory.getShape((ActionInsertNode) child,
                                                               isReadOnly);
            } else if (child instanceof ActionUpdateNode) {
                uiChildNode = actionUpdateNodeFactory.getShape((ActionUpdateNode) child,
                                                               isReadOnly);
            } else if (child instanceof ActionRetractNode) {
                uiChildNode = actionRetractNodeFactory.getShape((ActionRetractNode) child,
                                                                isReadOnly);
            }

            if (uiChildNode != null) {
                uiNode.addChildNode(uiChildNode);
                processChildren(child,
                                uiChildNode,
                                isReadOnly);
            }
        }
    }

    protected BaseGuidedDecisionTreeShape getParentNode(final BaseGuidedDecisionTreeShape uiChild,
                                                        final double cx,
                                                        final double cy) {
        BaseGuidedDecisionTreeShape uiProspectiveParent = null;
        double finalDistance = Double.MAX_VALUE;
        for (WiresBaseShape ws : getShapesInCanvas()) {
            if (ws.isVisible()) {
                if (ws instanceof BaseGuidedDecisionTreeShape) {
                    final BaseGuidedDecisionTreeShape uiNode = (BaseGuidedDecisionTreeShape) ws;
                    if (uiNode.acceptChildNode(uiChild) && !uiNode.hasCollapsedChildren()) {
                        double deltaX = cx - uiNode.getX();
                        double deltaY = cy - uiNode.getY();
                        double distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

                        if (finalDistance > distance) {
                            finalDistance = distance;
                            uiProspectiveParent = uiNode;
                        }
                    }
                }
            }
        }

        //If we're too far away from a parent we might as well not have a parent
        if (finalDistance > MAX_PROXIMITY) {
            uiProspectiveParent = null;
        }
        return uiProspectiveParent;
    }

    void layout() {
        //Get layout information
        final Map<WiresBaseShape, Point2D> layout = layoutManager.getLayoutInformation(uiRoot);
        final Rectangle2D canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas(layout);

        //Run an animation to move WiresBaseTreeNodes from their current position to the target position
        uiRoot.animate(AnimationTweener.EASE_OUT,
                       new AnimationProperties(),
                       ANIMATION_DURATION,
                       new IAnimationCallback() {

                           private final Map<WiresBaseShape, Pair<Point2D, Point2D>> transformations = new HashMap<WiresBaseShape, Pair<Point2D, Point2D>>();

                           @Override
                           public void onStart(final IAnimation iAnimation,
                                               final IAnimationHandle iAnimationHandle) {
                               //Reposition nodes. First we store the WiresBaseTreeNode together with its current position and target position
                               transformations.clear();
                               for (Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet()) {
                                   final Point2D origin = e.getKey().getLocation();
                                   final Point2D destination = new Point2D(e.getValue().getX(),
                                                                           e.getValue().getY());
                                   transformations.put(e.getKey(),
                                                       new Pair<Point2D, Point2D>(origin,
                                                                                  destination));
                               }
                               WiresLayoutUtilities.resizeViewPort(canvasBounds,
                                                                   canvasLayer.getViewport());
                           }

                           @Override
                           public void onFrame(final IAnimation iAnimation,
                                               final IAnimationHandle iAnimationHandle) {
                               //Lienzo's IAnimation.getPercent() passes values > 1.0
                               final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent();

                               //Move each descendant along the line between its origin and the target destination
                               for (Map.Entry<WiresBaseShape, Pair<Point2D, Point2D>> e : transformations.entrySet()) {
                                   final Point2D descendantOrigin = e.getValue().getK1();
                                   final Point2D descendantTarget = e.getValue().getK2();
                                   final double dx = (descendantTarget.getX() - descendantOrigin.getX()) * pct;
                                   final double dy = (descendantTarget.getY() - descendantOrigin.getY()) * pct;
                                   e.getKey().setX(descendantOrigin.getX() + dx);
                                   e.getKey().setY(descendantOrigin.getY() + dy);
                               }

                               //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode
                               uiRoot.getLayer().batch();
                           }

                           @Override
                           public void onClose(final IAnimation iAnimation,
                                               final IAnimationHandle iAnimationHandle) {
                               //Nothing to do
                           }
                       });

        canvasLayer.batch();
    }

    private void showGettingStartedHint() {
        if (isGettingStartedHintVisible) {
            return;
        }
        if (hint == null) {
            hint = new Group();
            final Rectangle hintRectangle = new Rectangle(600,
                                                          225,
                                                          15);
            hintRectangle.setStrokeWidth(2.0);
            hintRectangle.setStrokeColor("#6495ED");
            hintRectangle.setFillColor("#AFEEEE");
            hintRectangle.setAlpha(0.75);

            final Text hintText = new Text(GuidedDecisionTreeConstants.INSTANCE.gettingStartedHint(),
                                           ShapeFactoryUtil.FONT_FAMILY_DESCRIPTION,
                                           18);
            hintText.setTextAlign(TextAlign.CENTER);
            hintText.setTextBaseLine(TextBaseLine.MIDDLE);
            hintText.setFillColor("#6495ED");
            hintText.setX(hintRectangle.getWidth() / 2);
            hintText.setY(hintRectangle.getHeight() / 2);

            hint.setX((canvasLayer.getWidth() - hintRectangle.getWidth()) / 2);
            hint.setY((canvasLayer.getHeight() / 3) - (hintRectangle.getHeight() / 2));
            hint.add(hintRectangle);
            hint.add(hintText);
        }

        hint.animate(AnimationTweener.LINEAR,
                     new AnimationProperties(),
                     ANIMATION_DURATION,
                     new IAnimationCallback() {

                         @Override
                         public void onStart(final IAnimation iAnimation,
                                             final IAnimationHandle iAnimationHandle) {
                             hint.setAlpha(0.0);
                             canvasLayer.add(hint);
                             isGettingStartedHintVisible = true;
                         }

                         @Override
                         public void onFrame(final IAnimation iAnimation,
                                             final IAnimationHandle iAnimationHandle) {
                             //Lienzo's IAnimation.getPercent() passes values > 1.0
                             final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent();
                             hint.setAlpha(pct);
                             hint.getLayer().batch();
                         }

                         @Override
                         public void onClose(final IAnimation iAnimation,
                                             final IAnimationHandle iAnimationHandle) {
                             //Nothing to do
                         }
                     });
    }

    private void hideGettingStartedHint() {
        if (!isGettingStartedHintVisible) {
            return;
        }
        hint.animate(AnimationTweener.LINEAR,
                     new AnimationProperties(),
                     ANIMATION_DURATION,
                     new IAnimationCallback() {

                         @Override
                         public void onStart(final IAnimation iAnimation,
                                             final IAnimationHandle iAnimationHandle) {
                             //Nothing to do
                         }

                         @Override
                         public void onFrame(final IAnimation iAnimation,
                                             final IAnimationHandle iAnimationHandle) {
                             //Lienzo's IAnimation.getPercent() passes values > 1.0
                             final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent();
                             hint.setAlpha(1 - pct);
                             hint.getLayer().batch();
                         }

                         @Override
                         public void onClose(final IAnimation iAnimation,
                                             final IAnimationHandle iAnimationHandle) {
                             canvasLayer.remove(hint);
                             isGettingStartedHintVisible = false;
                         }
                     });
    }

    void setUiRoot(final WiresBaseTreeNode uiRoot) {
        this.uiRoot = uiRoot;
    }

    void setModel(final GuidedDecisionTree model) {
        this.model = model;
    }
}
